If you've built web apps for a while, "no JavaScript framework" probably sounds like a red flag. Fair — it usually is. But Dvl'epr isn't a framework with the JS ripped out. It's a different division of labour: you describe what a screen contains and where its data comes from, and the engine writes the code that makes it work.
This post covers the core loop, the file hierarchy every app is built from, and exactly what you stop having to write once the engine is doing its job.
The core loop
Every request to a Dvl'epr application runs the same cycle. There's no client-side router deciding what to render, and no separate API layer to keep in sync with the frontend — one engine does both, on every request:
Nothing above the SQL layer is hand-written imperative code. You're not calling fetch(), you're not writing a reducer, you're not wiring an event handler to a state update. You're declaring structure, and the engine's PHP core (never touched by app developers) walks that structure and does the wiring for you.
What an app actually is: five layers
A Dvl'epr application isn't one big file — it's a small hierarchy of XML files, each with one job. This is the entire shape of every app on the platform:
- App —
emxml_app[Role].xml— the outermost shell for a role (Manager, Sales Staff, Warehouse Staff…) - Site —
emxml_site[Role].xml— header, sidebar, nav structure shared across that role's pages - Page —
emxml_page[Role][Page].xml— one page's module registration - Module —
emxml_mod[Role][Module].xml— a screen's layout, and the only place JS gets generated - Block —
emxml_blk[Name].xml— a reusable content unit (a KPI card, a list, a detail panel) that any layer can pull in
That last point matters more than it looks: blocks are the unit of reuse. A "customer detail" block built for the CRM app can be dropped into a Sales dashboard or a Support ticket screen by reference, not by copy-paste — because every layer resolves to the same underlying tenant-scoped data.
A real block, simplified
Here's a trimmed version of a block from a live Bizheart build — an app built entirely on Dvl'epr. It's a KPI card pulling a live count from the database:
<Element type='inlinecontainer' tag='div'> <Attributes class='kpi-card' style='kpi-accent:var(brand-500)' /> <Element type='htmlitem' tag='div'> <Attributes class='kpi-label' /> <Text>Open Orders</Text> </Element> <Element type='htmlitem' tag='div'> <Attributes class='kpi-value' /> <Text>%open_order_count%</Text> </Element> </Element> <!-- SQL always goes after Layout --> <Query token='open_order_count'> SELECT COUNT(*) FROM orders WHERE dept_id = %dept_id% AND status = 'open' </Query>
Nobody wrote a query function, a component class, or a re-render trigger for this card. The %open_order_count% token gets resolved against the query result and substituted straight into the HTML the engine emits. Drop this block file into any module, on any app, for any tenant — it just works, scoped automatically to that tenant's dept_id.
What you stop writing
This is the part that actually changes how fast a full application comes together. Every one of the following is generated by the engine, not written by an app developer:
Never hand-written in a Dvl'epr app
getXxx() data-fetch functionsTake forms as an example. A modal form in a hand-built stack usually means: a component, a controlled-input state hook, a submit handler, a validation layer, an API call, a success toast, and a way to close the modal and refresh the underlying list. In Dvl'epr, that's an input block XML file plus a processor XML file. Register both, and the engine generates the getXxx() loader, the setupXxx() submit handler, validation against your declared required fields, and the success callback that closes the modal — because the pattern is identical every time, so it only had to be built correctly once, in the engine.
The instantiation chain, for the curious
Under the hood, one PHP call kicks off the whole page. There's no separate build step producing a bundle — the tree is walked and rendered on that request:
Site::initialise() ├── builds nav from <Menu> elements ├── walks <Layout> tree │ ├── inlinecontainer → static HTML wrapper │ ├── ajaxcontainer → generates the JS for this piece │ ├── block → Block::initialise(blk*.xml) │ └── page → Page → Module → Block(s) └── appends all generated JS to the page once, at the end
Under that structure, the same cycle runs for every piece of the tree: the platform executes the SQL a block declares, injects the resulting data straight into that block's HTML, and generates the JavaScript needed to wire it up — form handling, data loading, whatever the block calls for. Any developer-written JS is folded in alongside the generated JS. The result is assembled into the full page, or just the component, and sent to the browser ready to display. No block has to know how any other block gets its data or builds its JS — each one goes through the same pipeline independently, so they compose cleanly no matter how many are on a page.
Why this trade-off is worth it
It's worth noting that wherever a screen's structure — forms, lists, detail views, approvals, dashboards — is repetitive enough to describe declaratively, it's repetitive enough to generate. Against a traditional front-end framework, that means no state management code to write, no API client to hand-build and keep in sync with the backend, no build pipeline to babysit, and no framework version to upgrade out from under you later. A screen that would take a day of hand-rolled component work takes a single file containing the HTML structure and SQL — done in no time at all.
Anyone looking to build web applications faster, with a smaller team and fewer resources than a hand-rolled stack usually demands, will find that's exactly what the platform is built to deliver — regardless of the kind of application or the market it's aimed at.
Building a Data List: Using Only HTML Structure and SQL
Post 3 opens a short hands-on arc — a list, a table, and a detail screen, then a form and its processor, before post 8 pulls all of it together into one complete page.